package service;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: DELL
 * Date: 2023-07-01
 * Time: 8:38
 */
// 封装针对 fileMeta 表的操作
public class FileDao {
    // 1. 运行与用户电脑, 需要自动建表
    public void initDB() {
        // 1. 根据 sql.db 中读取语句
        // 2. 根据 sql 语句调用 jdbc 执行
        Connection connection = null;
        Statement statement = null; // 增删改查用 PreparedStatement, 其余建表之类的用 Statement
        try {
            // 1. 建立连接
            connection = DBUtil.getConnection();
            statement = connection.createStatement(); // Statement 使用  createStatement 来创建 Statement 对象

            // 2. 获取 sql.db 中的雨具
            String[] sqls = getInitSQL();
            for (String sql : sqls) {
                // System.out.println("[innitDB] sql : " + sql);
                // 读取 sqls 中的 sql 语句并执行
                statement.executeUpdate(sql);
            }

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(connection, statement, null);
        }
    }

    // 从 sql.db 中读取内容.
    // sql.db 中允许有多个 sql 语句
    private String[] getInitSQL() {

        // 用来存储最终结果
        StringBuilder stringBuilder = new StringBuilder();

        // 1. 确定文件路径, resources 包下的目录为 "Resources Root" - 资源文件根目录
        //    图片, 音频, 视频, 字体, 配置, 图标......
        // InoutStream 字节流, 此处我们的 sql.db 为一个字符流需要转换
        // FileDao.class.getClassLoader().getResourceAsStream 类对象的类加载器对象并打开某个文件
        // 放在 try 语句中, 可以自动进行关闭资源防止忘记释放资源
        try (InputStream inputStream = FileDao.class.getClassLoader().getResourceAsStream("db.sql")) {
            // 将 InputStream 字节流转为 InputStreamReader 字符流
            try (InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf8")) {
                while (true) {
                    // 返回值虽然是 int 类型, 但是读取到的 ch 实际上是一个 char 类型的字符
                    int ch = inputStreamReader.read(); // 读取内容, 返回值虽然是一个int 只是为了标记 -1 读取结束状态
                    if (ch == -1) {
                        // 读取结束, 结束循环
                        break;
                    }
                    // 每次读取一个字符并添加
                    stringBuilder.append((char) ch);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 转为数组以后, 按照分号分隔, 一个分号代表一个 SQL 语句执行完毕. 最终将整个文件切分成 SQL 数组
        return stringBuilder.toString().split(";");
    }


    // 2. 插入本地磁盘目录文件到数据库, 批量插入
    public void add(List<FileMeta> fileMetas) {
        // 批量操作可以使用循环来做, 但是太低效. 采取事务的形式来进行批量操作

        Connection connection = null;
        PreparedStatement preparedStatement = null;

        // 1. 将连接中的自动提交功能关闭.
        //    默认情况下, jdbc 中的 Connection 每次执行 execute 系列方法都会和数据库进行一次交互,
        //    这个过程中会针对数据库进行加锁等操作, 为了统一在最开始的时候进行加锁, 统一在最后进行一起解锁, 就需要关闭自动提交
        // jdbc 默认情况下每次执行一个 sql 都是一个事务, 只不过此时的事务中只有一个 sql
        try {
            // 和数据库建立连接并且关闭自动提交事务
            connection = DBUtil.getConnection();
            connection.setAutoCommit(false); // 设置为 false 关闭自动提交功能

            // 构造 sql 语句
            String sql = "insert into file_meta values(null, ?, ?, ?, ?, ?, ?, ?)";

            // 预执行
            preparedStatement = connection.prepareStatement(sql);

            // 替换 sql 占位符
            for (FileMeta fileMeta : fileMetas) {
                // 设置文件名
                preparedStatement.setString(1, fileMeta.getName());
                // 设置文件路径
                preparedStatement.setString(2, fileMeta.getPath());
                // 设置是否为目录
                preparedStatement.setBoolean(3, fileMeta.isDirectory());
                // 设置拼音
                preparedStatement.setString(4, fileMeta.getPinyin());
                // 设置拼音首字母
                preparedStatement.setString(5, fileMeta.getPinyinFirst());
                // 设置文件大小
                preparedStatement.setLong(6, fileMeta.getSize());
                // 设置修改时间
                // setTimestamp 无法直接接受一个 long 类型的数据, 需要将 long 类型时间转换为 TImestamp 类型
                preparedStatement.setTimestamp(7, new Timestamp(fileMeta.getLastModified()));

                // 2. 针对每个要执行的 SQL 使用 PreparedStatement 提供的 addBatch 方法来积累
                //    PreparedStatement 中是可以包含多个 sql 的, 这里的每个 sql 为一个 Batch
                preparedStatement.addBatch(); // 把上面构造的 sql 保存起来, 并且可以允许重新构造 sql
                System.out.println("[insert] 插入文件: " + fileMeta.getPath() + File.separator + fileMeta.getName());

            }
            // 3. 使用 executeBatch 统一执行上述所有的 Batch
            preparedStatement.executeBatch(); // 执行之前累积的 Batch

            // 4. 使用 commit 告知数据库执行完毕, 并且进行对数据库上的锁释放
            connection.commit();

        } catch (SQLException e) {
            // 5. 如果上述出现问题, 使用 rollback 进行回滚, 保证原子性
            try {
                // 未连接上则无法回滚
                if (connection != null) {
                    connection.rollback();
                }
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        } finally {
            DBUtil.close(connection, preparedStatement, null);
        }

    }

    // 3. 按照特定关键词 ( 全拼, 首字母, 或者一部分 ) 进行查询
    // 查询的 pattern 有可能是名字, 中文拼音 或者中文拼音首字母的一部分
    public List<FileMeta> searchByPattern(String pattern) {

        List<FileMeta> fileMetas = new ArrayList<>();
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
            connection = DBUtil.getConnection();

            // 进行全模糊匹配, 只要包含 pattern 即会被匹配到
            // 构造 sql
            String sql = "select name, path, is_directory, size, last_modified from file_meta"
                    + " where name like ? or pinyin like ? or pinyin_first like ?"
                    + " order by path, name";
            // 预执行
            preparedStatement = connection.prepareStatement(sql);

            // 替换占位符, 此处使用的时前后任意字符匹配, 会走取索引, 但是 %abc 或者 abc% 会使索引失效
            preparedStatement.setString(1, "%" + pattern + "%");
            preparedStatement.setString(2, "%" + pattern + "%");
            preparedStatement.setString(3, "%" + pattern + "%");

            resultSet = preparedStatement.executeQuery(); // 获取查询集合结果
            while (resultSet.next()) {

                // result 中存放的是查询到的符合条件的每一列
                String name = resultSet.getString("name");
                String path = resultSet.getString("path");
                boolean isDirectory = resultSet.getBoolean("is_directory");
                long size = resultSet.getLong("size");
                Timestamp lastModify = resultSet.getTimestamp("last_modified");

                // 构建文件对象
                FileMeta fileMeta = new FileMeta(name, path, isDirectory, size, lastModify.getTime());

                fileMetas.add(fileMeta); // 添加到集合中
            }

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(connection, preparedStatement, resultSet);
        }

        return fileMetas;
    }

    // 4. 给定路径进行扫描, 查询该路径下的所有结果.
    // 只会扫描该文件 即使该文件是目录 也不会展示该目录下的子文件
    // 后续反复扫描更新数据库时使用
    public List<FileMeta> searchByPath(String targetPath) {

        List<FileMeta> fileMetas = new ArrayList<>();

        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;

        try {
            connection = DBUtil.getConnection();

            // 构造 sql
            String sql = "select name, path, is_directory, size, last_modified from file_meta"
                    + " where path = ?";
            // 预处理
            preparedStatement = connection.prepareStatement(sql);
            // 替换占位符
            preparedStatement.setString(1, targetPath);
            // 执行查询获取结果集z
            resultSet = preparedStatement.executeQuery();

            while (resultSet.next()) {
                String name = resultSet.getString("name");
                String path = resultSet.getString("path");
                boolean isDirectory = resultSet.getBoolean("is_directory");
                long size = resultSet.getLong("size");
                Timestamp lastModify = resultSet.getTimestamp("last_modified");

                FileMeta fileMeta = new FileMeta(name, path, isDirectory, size, lastModify.getTime());

                fileMetas.add(fileMeta);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(connection, preparedStatement, resultSet);
        }

        return fileMetas;
    }

    // 5. 批量删除文件- 用户在本地磁盘上进行了删除操作, 因此需要根据路径来进行更新
    // 删除的可能是一个普通文件直接进行删除 ( 直接删除对应的表记录即可 )
    // 如果是一个目录, 那么需要删除这个目录下的所有普通文件和子目录
    public void delete(List<FileMeta> fileMetas) {
        Connection connection = null;
        PreparedStatement preparedStatement = null;

        try {
            connection = DBUtil.getConnection();

            // 1. 关闭自动提交
            connection.setAutoCommit(false);

            // 2. 删除目录或者普通文件
            for (FileMeta fileMeta : fileMetas) {
                String sql = null;
                if (!fileMeta.isDirectory()) {
                    // 不是一个目录
                    sql = "delete from file_meta where name = ? and path = ? ";
                } else {
                    // 为一个目录
                    // 删除文件本身已经模糊匹配子文件 path : e:/test %, 以 e:/test 开头的都删除
                    // where (name = ? and path = ?)  删除当前这个目录本身
                    // path like ? 根据 path 删除当前这个文件下的子文件和子目录
                    sql = "delete from file_meta where (name = ? and path = ?) or (path like ?)";

                }

                // 使用 addBatch 的前提是 sql 是一个固定的模板
                // 此处使用 ? 进行占位符替换以后, 不一定是一个相同的 sql 模板 ( 即针对文件还是目录不一样 sql )
                // 因此只能使用重新构造 sql

                // 预处理 sql
                preparedStatement = connection.prepareStatement(sql);

                // 替换占位符
                if (!fileMeta.isDirectory()) {
                    // 文件情况
                    preparedStatement.setString(1, fileMeta.getName());
                    preparedStatement.setString(2, fileMeta.getPath());
                } else {

                    // TODO : e:/a/b/... 现在删除的是 b 这个目录, 那么这里的删除子文件夹就会出问题
                    // 此时针对 b 这个目录来说, name: b  path: e:/a
                    // 因此这里要写成 e:/a/b/% 这样才能删除 b 目录下面的子文件
                    // path like e:/a/b/'%'  -> path + '/' + name + '/' + '%' 的 sql 形式

                    // 目录情况
                    preparedStatement.setString(1, fileMeta.getName());
                    preparedStatement.setString(2, fileMeta.getPath());
                    preparedStatement.setString(3, fileMeta.getPath() + File.separator + fileMeta.getName() + File.separator + "%");
                }

                // 执行 sql 删除操作
                preparedStatement.executeUpdate();
                System.out.println("删除成功 : " + fileMeta.getPath() + File.separator + fileMeta.getName());
                preparedStatement.close(); // 上述中代码里有多个 Statement 对象因此需要关闭多次, 因为 sql 有多个不同的模板情况
            }

            // 事务执行成功主动 commit
            connection.commit();

        } catch (SQLException e) {
            try {
                if (connection != null) {
                    connection.rollback();
                }
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        } finally {
            DBUtil.close(connection, null, null);
        }
    }

}
